/* *******************************************************************
 * Copyright (c) 1999-2001 Xerox Corporation,
 *               2002 Palo Alto Research Center, Incorporated (PARC).
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Xerox/PARC     initial implementation
 * ******************************************************************/

package org.aspectj.testing.harness.bridge;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.testing.util.TestUtil;
import org.aspectj.testing.xml.IXmlWritable;
import org.aspectj.testing.xml.XMLWriter;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;

/**
 * Calculate changes in a directory tree.
 * This implements two different specification styles:
 * <ul>
 * <li>Specify files added, removed, updated, and/or a component
 *     to check any existing files</li>
 * <li>Specify expected directory.  When complete this checks that
 *     any files in expected directory are matched in the actual.
 *     (.class files are dissassembled before comparison.)
 * </li>
 * </ul>
 * Usage:
 * <ul>
 * <li>Set up with any expected changes and/or an expected directory</li>
 * <li>Set up with any file checker</li>
 * <li>start(..) before changes.
 *     This issues messages for any removed files not found,
 *     which represent an error in the expected changes.</li>
 * <li>Do whatever operations will change the directory</li>
 * <li>end(..).
 *     This issues messages for any files not removed, added, or updated,
 *     and, if any checker was set, any checker messages for matching
 *     added or updated files</li>
 * </ul>
 * When comparing directories, this ignores any paths containing "CVS".
 */
public class DirChanges {

    public static final String DELAY_NAME = "dir-changes.delay";
    private static final long DELAY;
    static {
        long delay = 10l;
        try {
            delay = Long.getLong(DELAY_NAME);
            if ((delay > 40000) || (delay < 0)) {
                delay = 10l;
            }
        } catch (Throwable t) {
            // ignore
        }
        DELAY = delay;
    }

	private static final boolean EXISTS = true;

    final Spec spec;

    /** start time, in milliseconds - valid only from start(..)..end(..) */
    long startTime;

    /** base directory of actual files - valid only from start(..)..end(..) */
    File baseDir;

    /** if set, this is run against any resulting existing files
     * specified in added/updated lists.
     * This does not affect expected-directory comparison.
     */
    IFileChecker fileChecker;

    /** handler valid from start..end of start(..) and end(..) methods */
    IMessageHandler handler;

	/**
	 * Constructor for DirChanges.
	 */
	public DirChanges(Spec spec) {
		LangUtil.throwIaxIfNull(spec, "spec");
        this.spec = spec;
    }

    /**
     * Inspect the base dir, and issue any messages for
     * removed files not present.
     * @param baseDir File for a readable directory
     * @return true if this started without sending messages
     *          for removed files not present.
     */
    public boolean start(IMessageHandler handler, File baseDir) {
        FileUtil.throwIaxUnlessCanReadDir(baseDir, "baseDir");
        final IMessageHandler oldHandler = this.handler;
        this.handler = handler;
        this.baseDir = baseDir;
        startTime = 0l;
        final boolean doCompare = false;
        boolean result
                = exists("at start, did not expect added file to exist", !EXISTS, spec.added, doCompare);
        result &= exists("at start, expected unchanged file to exist", EXISTS, spec.unchanged, doCompare);
        result &= exists("at start, expected updated file to exist", EXISTS, spec.updated, doCompare);
        result &= exists("at start, expected removed file to exist", EXISTS, spec.removed, doCompare);
        startTime = System.currentTimeMillis();
        // ensure tests don't complete in < 1 second, otherwise can confuse fast machines.
        try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        this.handler = oldHandler;
        return result;
    }


    /**
     * Inspect the base dir, issue any messages for
     * files not added, files not updated, and files not removed,
     * and compare expected/actual files added or updated.
     * This sleeps before checking until at least DELAY milliseconds after start.
     * @throws IllegalStateException if called before start(..)
     */
    public boolean end(IMessageHandler handler, File srcBaseDir) {
        FileUtil.throwIaxUnlessCanReadDir(baseDir, "baseDir");
        if (0l == startTime) {
            throw new IllegalStateException("called before start");
        }
        final long targetTime = startTime + spec.delayInMilliseconds;
        do {
            long curTime = System.currentTimeMillis();
            if (curTime >= targetTime) {
                break;
            }
            try {
				Thread.sleep(targetTime-curTime);
			} catch (InterruptedException e) {
                break;
            }
        } while (true);
        final IMessageHandler oldHandler = this.handler;
        this.handler = handler;
        try {
            // variant 1: check specified files
            // deferring comparison to end...
            final boolean doCompare = (null != fileChecker);
            final boolean fastFail = spec.fastFail;
            boolean result
                    = exists("at end, expected file was not added",   EXISTS, spec.added, doCompare);
            if (result || !fastFail) {
                result &= exists("at end, expected file was not unchanged", EXISTS, spec.unchanged, doCompare, false);
            }
            if (result || !fastFail) {
                result &= exists("at end, expected file was not updated", EXISTS, spec.updated, doCompare);
            }
            if (result || !fastFail) {
                result &= exists("at end, file exists, was not removed", !EXISTS, spec.removed, doCompare);
            }
//            if (result || !fastFail) {
//                // XXX validate that unchanged mod-time did not change
//            }
            // variant 1: compare expected directory
            if (result || !fastFail) {
                result &= compareDir(srcBaseDir);
            }
            return result;
        } finally {
            this.handler = oldHandler;
            baseDir = null;
            startTime = 0l;
        }
    }

	/**
     * Verify that all files in any specified expected directory
     * have matching files in the base directory, putting any messages
     * in the handler (only one if the spec indicates fast-fail).
	 * @param srcBaseDir the File for the base directory of the test sources
     *         (any expected dir is specified relative to this directory)
	 * @return true if the same, false otherwise
	 */
	private boolean compareDir(File srcBaseDir) {
        if (null == spec.expDir) {
            return true;
        }
        File expDir = new File(srcBaseDir, spec.expDir);
        File actDir = baseDir;
        //System.err.println("XXX comparing actDir=" + actDir + " expDir=" + expDir);
        return TestUtil.sameDirectoryContents(handler, expDir, actDir, spec.fastFail);
	}


    /** @param comp FileMessageComparer (if any) given matching messages to compare */
    protected void setFileComparer(IFileChecker comp) {
        this.fileChecker = comp;
    }



    /**
     * Signal fail if any files do {not} exist or do {not} have last-mod-time after startTime
     * @param handler the IMessageHandler sink for messages
     * @param label the String infix for the fail message
     * @param exists if true, then file must exist and be modified after start time;
     *                if false, then the file must not exist or must be modified before start time.
     * @param pathList the List of path (without any Spec.defaultSuffix) of File
     *                  in Spec.baseDir to find (or not, if !exists)
     */
    protected boolean exists(
        String label,
        boolean exists,
        List pathList,
        boolean doCompare) {
//        boolean expectStartEarlier = true;
      	return exists(label, exists, pathList,doCompare, true);
    }
    protected boolean exists(
        String label,
        boolean exists,
        List pathList,
        boolean doCompare,
        boolean expectStartEarlier) {
        boolean result = true;
        if (!LangUtil.isEmpty(pathList)) {
//            final File expDir = ((!doCompare || (null == spec.expDir))
//                ? null
//                : new File(baseDir, spec.expDir));
			for (Object o : pathList) {
				final String entry = (String) o;
				String path = entry;
				if (null != spec.defaultSuffix) {
					if (".class".equals(spec.defaultSuffix)) {
						path = path.replace('.', '/');
					}
					path = path + spec.defaultSuffix;
				}
				File actualFile = new File(baseDir, path);
				if (exists != (actualFile.canRead() && actualFile.isFile()
						&& (expectStartEarlier
						? startTime <= actualFile.lastModified()
						: startTime > actualFile.lastModified()
				))) {
					failMessage(handler, exists, label, path, actualFile);
					if (result) {
						result = false;
					}
				} else if (exists && doCompare && (null != fileChecker)) {
					if (!fileChecker.checkFile(handler, path, actualFile) && result) {
						result = false;
					}
				}
			}
        }
        return result;
    }

    /**
     * Generate fail message "{un}expected {label} file {path} in {baseDir}".
     * @param handler the IMessageHandler sink
     * @param label String message infix
     * @param path the path to the file
     */
    protected void failMessage(
        IMessageHandler handler,
        boolean exists,
        String label,
        String path,
        File file) {
        MessageUtil.fail(handler, label + " \"" + path + "\" in " + baseDir);
    }

    /** Check actual File found at a path, usually to diff expected/actual contents */
    public interface IFileChecker {
        /**
         * Check file found at path.
         * Implementations should return false when adding fail (or worse)
         * message to the handler, and true otherwise.
         * @param handler IMessageHandler sink for messages, esp. fail.
         * @param path String for normalized path portion of actualFile.getPath()
         * @param actualFile File to file found
         * @return false if check failed and messages added to handler
         */
        boolean checkFile(IMessageHandler handler, String path, File actualFile);
    }

    /**
     * Specification for a set of File added, removed, or updated
     * in a given directory, or for a directory base for a tree of expected files.
     * If defaultSuffix is specified, entries may be added without it.
     * Currently the directory tree
     * only is used to verify files that are expected
     * and found after the process completes.
     */
    public static class Spec implements IXmlWritable {
        /** XML element name */
        public static final String XMLNAME = "dir-changes";

         /** a symbolic name for the base directory */
        String dirToken; // XXX default to class?

        /** if set, then append to specified paths when seeking files */
        String defaultSuffix;

        /** relative path of dir with expected files for comparison */
        String expDir;

        long delayInMilliseconds = DELAY;

        /** if true, fail on first mis-match */
        boolean fastFail;

        /** relative paths (String) of expected files added */
        final List<String> added;

        /** relative paths (String) of expected files removed/deleted */
        final List<String> removed;

        /** relative paths (String) of expected files updated/changed */
        final List<String> updated;

        /** relative paths (String) of expected files NOT
         * added, removed, or changed
         * XXX unchanged unimplemented
         */
        final List<String> unchanged;

        public Spec() {
            added = new ArrayList<>();
            removed = new ArrayList<>();
            updated = new ArrayList<>();
            unchanged = new ArrayList<>();
        }

        /**
         * @param dirToken the symbol name of the base directory (classes, run)
         */
        public void setDirToken(String dirToken) {
            this.dirToken = dirToken;
        }

        /**
         * Set the directory containing the expected files.
         * @param expectedDirRelativePath path relative to the test base
         *         of the directory containing expected results for the output dir.
         */
        public void setExpDir(String expectedDirRelativePath) {
            expDir = expectedDirRelativePath;
        }

        public void setDelay(String delay) {
            if (null != delay) {
                // let NumberFormatException propogate up
                delayInMilliseconds = Long.parseLong(delay);
                if (delayInMilliseconds < 0l) {
                    delayInMilliseconds = 0l;
                }
            }
        }

        /**
         * @param clipSuffix the String suffix, if any, to clip automatically
         */
        public void setDefaultSuffix(String defaultSuffix) {
            this.defaultSuffix = defaultSuffix;
        }

        public void setAdded(String items) {
            XMLWriter.addFlattenedItems(added, items);
        }

        public void setRemoved(String items) {
            XMLWriter.addFlattenedItems(removed, items);
        }

        public void setUpdated(String items) {
            XMLWriter.addFlattenedItems(updated, items);
        }

        public void setUnchanged(String items) {
            XMLWriter.addFlattenedItems(unchanged, items);
        }
        public void setFastfail(boolean fastFail) {
            this.fastFail = fastFail;
        }

        /** @return true if some list was specified */
        private boolean hasFileList() {
            return (!LangUtil.isEmpty(added)
            		|| !LangUtil.isEmpty(removed)
                    || !LangUtil.isEmpty(updated)
                    || !LangUtil.isEmpty(unchanged)
                    );
        }

        /**
         * Emit specification in XML form if not empty.
         * This writes nothing if there is no expected dir
         * and there are no added, removed, or changed.
         * fastFail is written only if true, since the default is false.
         */
        public void writeXml(XMLWriter out) {
            if (!hasFileList()  && LangUtil.isEmpty(expDir)) {
                return;
            }
            // XXX need to permit defaults here...
            out.startElement(XMLNAME, false);
            if (!LangUtil.isEmpty(dirToken)) {
                out.printAttribute("dirToken", dirToken.trim());
            }
            if (!LangUtil.isEmpty(defaultSuffix)) {
                out.printAttribute("defaultSuffix", defaultSuffix.trim());
            }
            if (!LangUtil.isEmpty(expDir)) {
                out.printAttribute("expDir", expDir.trim());
            }
            if (!LangUtil.isEmpty(added)) {
                out.printAttribute("added", XMLWriter.flattenList(added));
            }
            if (!LangUtil.isEmpty(removed)) {
                out.printAttribute("removed", XMLWriter.flattenList(removed));
            }
            if (!LangUtil.isEmpty(updated)) {
                out.printAttribute("updated", XMLWriter.flattenList(updated));
            }
            if (!LangUtil.isEmpty(unchanged)) {
                out.printAttribute("unchanged", XMLWriter.flattenList(unchanged));
            }
            if (fastFail) {
                out.printAttribute("fastFail", "true");
            }
            out.endElement(XMLNAME);
        }

        /**
		 * Write list as elements to XMLWriter.
		 * @param out XMLWriter output sink
		 * @param dirChanges List of DirChanges.Spec to write
		 */
		public static void writeXml(XMLWriter out, List<DirChanges.Spec> dirChanges) {
            if (LangUtil.isEmpty(dirChanges)) {
                return;
            }
            LangUtil.throwIaxIfNull(out, "out");
			for (Spec spec : dirChanges) {
				if (null == spec) {
					continue;
				}
				spec.writeXml(out);
			}
		}

} // class Spec

}
